# Copyright (c) 2008 Giorgos Verigakis <verigak@gmail.com>
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

from ctypes import *
from ctypes.util import find_library
from errno import *
from functools import partial
from pdb import set_trace
from platform import machine, system
from stat import S_IFDIR
import contextlib
import ctypes
import pdb
import errno
import functools
import logging
import os
import signal
import time
import traceback

logger = logging.getLogger('fuse')


class Structure(ctypes.Structure):

    def __repr__(self):
        return '{}({})'.format(
            self.__class__.__name__,
            ', '.join(('{}={!r}'.format(m, v) for (m,v) in ((i[0], getattr(self, i[0])) for i in self._fields_) if v)))

    def __bool__(self):
        return any(getattr(self, a) for a in (b[0] for b in self._fields_))


class c_timespec(Structure):
    _fields_ = [('tv_sec', c_long), ('tv_nsec', c_long)]

class c_utimbuf(Structure):
    _fields_ = [('actime', c_timespec), ('modtime', c_timespec)]

class c_stat(Structure):
    pass    # Platform dependent

_system = system()
if _system in ('Darwin', 'FreeBSD'):
    _libiconv = CDLL(find_library("iconv"), RTLD_GLOBAL)     # libfuse dependency
    ENOTSUP = 45
    c_dev_t = c_int32
    c_fsblkcnt_t = c_ulong
    c_fsfilcnt_t = c_ulong
    c_gid_t = c_uint32
    c_mode_t = c_uint16
    c_off_t = c_int64
    c_pid_t = c_int32
    c_uid_t = c_uint32
    setxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte),
        c_size_t, c_int, c_uint32)
    getxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte),
        c_size_t, c_uint32)
    c_stat._fields_ = [
        ('st_dev', c_dev_t),
        ('st_ino', c_uint32),
        ('st_mode', c_mode_t),
        ('st_nlink', c_uint16),
        ('st_uid', c_uid_t),
        ('st_gid', c_gid_t),
        ('st_rdev', c_dev_t),
        ('st_atimespec', c_timespec),
        ('st_mtimespec', c_timespec),
        ('st_ctimespec', c_timespec),
        ('st_size', c_off_t),
        ('st_blocks', c_int64),
        ('st_blksize', c_int32)]
elif _system == 'Linux':
    ENOTSUP = 95
    c_dev_t = c_ulonglong
    c_fsblkcnt_t = c_ulonglong
    c_fsfilcnt_t = c_ulonglong
    c_gid_t = c_uint
    c_mode_t = c_uint
    c_off_t = c_longlong
    c_pid_t = c_int
    c_uid_t = c_uint
    setxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t, c_int)
    getxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t)

    _machine = machine()
    if _machine == 'x86_64':
        c_stat._fields_ = [
            ('st_dev', c_dev_t),
            ('st_ino', c_ulong),
            ('st_nlink', c_ulong),
            ('st_mode', c_mode_t),
            ('st_uid', c_uid_t),
            ('st_gid', c_gid_t),
            ('__pad0', c_int),
            ('st_rdev', c_dev_t),
            ('st_size', c_off_t),
            ('st_blksize', c_long),
            ('st_blocks', c_long),
            ('st_atimespec', c_timespec),
            ('st_mtimespec', c_timespec),
            ('st_ctimespec', c_timespec)]
    elif _machine == 'ppc':
        c_stat._fields_ = [
            ('st_dev', c_dev_t),
            ('st_ino', c_ulonglong),
            ('st_mode', c_mode_t),
            ('st_nlink', c_uint),
            ('st_uid', c_uid_t),
            ('st_gid', c_gid_t),
            ('st_rdev', c_dev_t),
            ('__pad2', c_ushort),
            ('st_size', c_off_t),
            ('st_blksize', c_long),
            ('st_blocks', c_longlong),
            ('st_atimespec', c_timespec),
            ('st_mtimespec', c_timespec),
            ('st_ctimespec', c_timespec)]
    else:
        # i686, use as fallback for everything else
        c_stat._fields_ = [
            ('st_dev', c_dev_t),
            ('__pad1', c_ushort),
            ('__st_ino', c_ulong),
            ('st_mode', c_mode_t),
            ('st_nlink', c_uint),
            ('st_uid', c_uid_t),
            ('st_gid', c_gid_t),
            ('st_rdev', c_dev_t),
            ('__pad2', c_ushort),
            ('st_size', c_off_t),
            ('st_blksize', c_long),
            ('st_blocks', c_longlong),
            ('st_atimespec', c_timespec),
            ('st_mtimespec', c_timespec),
            ('st_ctimespec', c_timespec),
            ('st_ino', c_ulonglong)]
else:
    raise NotImplementedError('%s is not supported.' % _system)


class c_statvfs(Structure):
    _fields_ = [
        ('f_bsize', c_ulong),
        ('f_frsize', c_ulong),
        ('f_blocks', c_fsblkcnt_t),
        ('f_bfree', c_fsblkcnt_t),
        ('f_bavail', c_fsblkcnt_t),
        ('f_files', c_fsfilcnt_t),
        ('f_ffree', c_fsfilcnt_t),
        ('f_favail', c_fsfilcnt_t)]

if _system == 'FreeBSD':
    c_fsblkcnt_t = c_uint64
    c_fsfilcnt_t = c_uint64
    setxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t, c_int)
    getxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t)
    class c_statvfs(Structure):
        _fields_ = [
            ('f_bavail', c_fsblkcnt_t),
            ('f_bfree', c_fsblkcnt_t),
            ('f_blocks', c_fsblkcnt_t),
            ('f_favail', c_fsfilcnt_t),
            ('f_ffree', c_fsfilcnt_t),
            ('f_files', c_fsfilcnt_t),
            ('f_bsize', c_ulong),
            ('f_flag', c_ulong),
            ('f_frsize', c_ulong)]

class fuse_file_info(Structure):
    _fields_ = [
        ('flags', c_int),
        ('fh_old', c_ulong),
        ('writepage', c_int),
        ('direct_io', c_uint, 1),
        ('keep_cache', c_uint, 1),
        ('flush', c_uint, 1),
        ('padding', c_uint, 29),
        ('fh', c_uint64),
        ('lock_owner', c_uint64)]

class fuse_context(Structure):
    _fields_ = [
        ('fuse', c_voidp),
        ('uid', c_uid_t),
        ('gid', c_gid_t),
        ('pid', c_pid_t),
        ('private_data', c_voidp)]

class fuse_operations(Structure):
    _fields_ = [
        ('getattr',     CFUNCTYPE(c_int, c_char_p, POINTER(c_stat))),
        ('readlink',    CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t)),
        ('getdir',      c_voidp), # Deprecated, use readdir
        ('mknod',       CFUNCTYPE(c_int, c_char_p, c_mode_t, c_dev_t)),
        ('mkdir',       CFUNCTYPE(c_int, c_char_p, c_mode_t)),
        ('unlink',      CFUNCTYPE(c_int, c_char_p)),
        ('rmdir',       CFUNCTYPE(c_int, c_char_p)),
        ('symlink',     CFUNCTYPE(c_int, c_char_p, c_char_p)),
        ('rename',      CFUNCTYPE(c_int, c_char_p, c_char_p)),
        ('link',        CFUNCTYPE(c_int, c_char_p, c_char_p)),
        ('chmod',       CFUNCTYPE(c_int, c_char_p, c_mode_t)),
        ('chown',       CFUNCTYPE(c_int, c_char_p, c_uid_t, c_gid_t)),
        ('truncate',    CFUNCTYPE(c_int, c_char_p, c_off_t)),
        ('utime',       c_voidp), # Deprecated, use utimens
        ('open',        CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))),
        ('read',        CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t, c_off_t, POINTER(fuse_file_info))),
        ('write',       CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t, c_off_t, POINTER(fuse_file_info))),
        ('statfs',      CFUNCTYPE(c_int, c_char_p, POINTER(c_statvfs))),
        ('flush',       CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))),
        ('release',     CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))),
        ('fsync',       CFUNCTYPE(c_int, c_char_p, c_int, POINTER(fuse_file_info))),
        ('setxattr',    setxattr_t),
        ('getxattr',    getxattr_t),
        ('listxattr',   CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t)),
        ('removexattr', CFUNCTYPE(c_int, c_char_p, c_char_p)),
        ('opendir',     CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))),
        ('readdir',     CFUNCTYPE(c_int, c_char_p, c_voidp,
                                  CFUNCTYPE(c_int, c_voidp, c_char_p, POINTER(c_stat), c_off_t),
                                  c_off_t, POINTER(fuse_file_info))),
        ('releasedir',  CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))),
        ('fsyncdir',    CFUNCTYPE(c_int, c_char_p, c_int, POINTER(fuse_file_info))),
        ('init',        CFUNCTYPE(c_voidp, c_voidp)), # TODO implement fuse_conn_info
        ('destroy',     CFUNCTYPE(c_voidp, c_voidp)),
        ('access',      CFUNCTYPE(c_int, c_char_p, c_int)),
        ('create',      CFUNCTYPE(c_int, c_char_p, c_mode_t, POINTER(fuse_file_info))),
        ('ftruncate',   CFUNCTYPE(c_int, c_char_p, c_off_t, POINTER(fuse_file_info))),
        ('fgetattr',    CFUNCTYPE(c_int, c_char_p, POINTER(c_stat), POINTER(fuse_file_info))),
        ('lock',        CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info), c_int, c_voidp)),
        ('utimens',     CFUNCTYPE(c_int, c_char_p, POINTER(2 * c_timespec))),
        ('bmap',        CFUNCTYPE(c_int, c_char_p, c_size_t, POINTER(c_ulonglong)))]


def time_from_timespec(ts):
    return ts.tv_sec + ts.tv_nsec / 10 ** 9

_libfuse_path = find_library('fuse')
if not _libfuse_path:
    raise EnvironmentError('Unable to find libfuse')
_libfuse = CDLL(_libfuse_path)
_libfuse.fuse_get_context.restype = POINTER(fuse_context)


def fuse_get_context():
    """Returns a (uid, gid, pid) tuple"""
    ctxp = _libfuse.fuse_get_context()
    ctx = ctxp.contents
    return ctx.uid, ctx.gid, ctx.pid

@contextlib.contextmanager
def fuse_friendly_signal_handlers():
    '''
    Put in default signal handlers so that fuse installs nice ones of its own.
    See fuse/lib/fuse_signals.c.
    Restore the original handlers after exiting the context.
    '''
    old_signal_handlers = {}
    for sig in 'HUP', 'INT', 'TERM', 'PIPE':
        signum = getattr(signal, 'SIG'+sig)
        old_signal_handlers[signum] = signal.signal(signum, signal.SIG_DFL)
    yield
    # restore python handlers or whatever was in use previously
    # fuse should restore all the modified signals to the defaults on exit
    for signum, handler in old_signal_handlers.items():
        cur_handler = signal.signal(signum, handler)
        logger.debug('Restored old signal %s handler %s', signum, handler)
        if cur_handler != signal.SIG_DFL:
            logger.warning('Signal %s handler is %r not SIG_DFL as expected', signum, cur_handler)

def print_fuse_opts():
    argv = [b'blah', b'-ho']
    argv = (c_char_p * len(argv))(*argv)
    _libfuse.fuse_main_real(len(argv), argv, None, 0, None)


class FUSE:

    def __init__(self, operations, args, logger=None):
        if logger is None:
            logger = logging.getLogger('fuse')
        self.logger = logger

        fuse_ops = fuse_operations()
        for name, prototype in fuse_operations._fields_:
            if prototype == c_voidp:
                continue
            setattr(fuse_ops, name, prototype(partial(self._op_wrapper, name)))
        self.operations = operations

        argv = (c_char_p * len(args))(*(arg.encode('utf-8') for arg in args))
        with fuse_friendly_signal_handlers():
            # we don't return from here until the filesystem is unmounted
            _libfuse.fuse_main_real(
                len(args), argv,
                pointer(fuse_ops), ctypes.sizeof(fuse_ops),
                c_void_p(None))

    def _op_wrapper(self, opname, *args, **kwargs):
        try:
            if hasattr(self, opname):
                args, kwargs = getattr(self, opname)(*args, **kwargs)
            handler = getattr(self.operations, opname, None)
            if handler is None:
                self.logger.info('%r not implemented', opname)
                return -errno.ENOSYS
            retval = handler(*args, **kwargs)
            if retval is None:
                retval = 0
            logrv = retval if retval >= 0 else errno.errorcode[-retval]
            self.logger.debug('%s->%s', opname, logrv)
            return retval
        except:
            traceback.print_exc()
            pdb.post_mortem()
            os.kill(os.getpid(), signal.SIGINT)
            return -errno.EFAULT

    def chown(self, path, uid, gid):
        if uid == c_uid_t(-1).value:
            uid = -1
        if gid == c_gid_t(-1).value:
            gid = -1
        return [path, uid, gid], {}

    def flush(self, path, fi):
        return [path, fi.contents], {}

    # TODO: flock is not handled
    def lock(self, path, fileinfo, cmd, flock):
        return [path, fileinfo.contents, cmd, flock], {}

    def readdir(self, path, buf, filler, off, fi):
        return [path, functools.partial(filler, buf), off, fi.contents, c_stat], {}

    def getattr(self, path, stat):
        return [path, stat.contents], {}

    def fgetattr(self, path, stat, fi):
        return [path, stat.contents, fi.contents], {}

    def releasedir(self, path, fi):
        return [path, fi.contents], {}

    def opendir(self, path, fi):
        return [path, fi.contents], {}

    def statfs(self, path, statfs):
        return [path, statfs.contents], {}

    def open(self, path, fi):
        return [path, fi.contents], {}

    def create(self, path, mode, fi):
        return [path, mode, fi.contents], {}

    def release(self, path, fi):
        return [path, fi.contents], {}

    def utimens(self, path, times):
        return [path, (
            time_from_timespec(times.contents[0]),
            time_from_timespec(times.contents[1]))], {}

    def read(self, path, buf, nbyte, offset, fi):
        return [path, buf, nbyte, offset, fi.contents], {}

    def write(self, path, buf, nbyte, offset, fi):
        return [path, string_at(buf, nbyte), offset, fi.contents], {}

    def ftruncate(self, path, length, fi):
        return [path, length, fi.contents], {}
